package com.ruoyi.web.util;


import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;

/**
 * 功能描述:
 * SM2公钥加密算法实现 包括 -签名,验签 -密钥交换 -公钥加密,私钥解密
 *
 * @Param:
 * @Return:
 * @Author: bin
 * @Date: 2020/7/27 21:15
 */
@Slf4j
public class SM2Utils {
    private static BigInteger n = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "7203DF6B" + "21C6052B" + "53BBF409" + "39D54123", 16);
    private static BigInteger p = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFF", 16);
    private static BigInteger a = new BigInteger(
            "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFC", 16);
    private static BigInteger b = new BigInteger(
            "28E9FA9E" + "9D9F5E34" + "4D5A9E4B" + "CF6509A7" + "F39789F5" + "15AB8F92" + "DDBCBD41" + "4D940E93", 16);
    private static BigInteger gx = new BigInteger(
            "32C4AE2C" + "1F198119" + "5F990446" + "6A39C994" + "8FE30BBF" + "F2660BE1" + "715A4589" + "334C74C7", 16);
    private static BigInteger gy = new BigInteger(
            "BC3736A2" + "F4F6779C" + "59BDCEE3" + "6B692153" + "D0A9877C" + "C62A4740" + "02DF32E5" + "2139F0A0", 16);

    private static int w = (int) Math.ceil(n.bitLength() * 1.0 / 2) - 1;
    private static BigInteger _2w = new BigInteger("2").pow(w);
    private static final int DIGEST_LENGTH = 32;
    private static SecureRandom random = new SecureRandom();
    private static ECCurve.Fp curve = new ECCurve.Fp(p, a, b);
    private static ECPoint G = curve.createPoint(gx, gy);
    private static ECDomainParameters ecc_bc_spec = new ECDomainParameters(curve, G, n);
    private static boolean debug = false;

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        SM2Utils.debug = debug;
    }

    /**
     * 以16进制打印字节数组
     *
     * @param b
     */
    public static void printHexString(byte[] b) {
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
        }
    }

    public static void main(String[] args) throws Exception {
//        String singn = "30460221008404d071f6f96943d7c7b01aa9d3ef6e1c7a757baf5953ac34841eff6817e43f022100de3f1a6f653f09906f911d80580257f9bf55b452e7cf3a98e54f7fcb1791311d";
//        String data = "151071";
//        String pub = "03cedcc9857ca27ea1c1ddc36f9c4f8dc747b237805e1465f35cf2d47c0d2e3b0f";
        String singn = "3046022100ef807efbda1daec05d6dbf53f6d2a6eb8d674086dc2216d6693b1c180cdd48ef02210095773087455a2e3e9e12528c4a59bd2e79d504efb2339640f28e7ab2db457ac1";
        String data = "151071";
        String pub = "03cedcc9857ca27ea1c1ddc36f9c4f8dc747b237805e1465f35cf2d47c0d2e3b0f";
        verify(data,singn,data,pub);
        System.out.println(verify(data,singn,data,pub));
    }

    /**
     * SM2公私钥对
     */
    public static final class SM2KeyPair {
        private String publicKey;
        private String privateKey;

        public SM2KeyPair() {
        }

        public SM2KeyPair(String publicKey, String privateKey) {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }

        public String getPublicKey() {
            return publicKey;
        }

        public String getPrivateKey() {
            return privateKey;
        }
    }

    /**
     * 随机数生成器
     *
     * @param max
     * @return
     */
    private static BigInteger random(BigInteger max) {

        BigInteger r = new BigInteger(256, random);

        while (r.compareTo(max) >= 0) {
            r = new BigInteger(128, random);
        }

        return r;
    }

    /**
     * 判断字节数组是否全0
     *
     * @param buffer
     * @return
     */
    private static boolean allZero(byte[] buffer) {
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * 公钥加密
     * * @param input
     * 加密原文
     *
     * @param publicKey 公钥
     * @return
     */
    public static byte[] encrypt(String input, ECPoint publicKey) {

        byte[] inputBuffer = new byte[0];
        try {
            inputBuffer = input.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            System.out.println("加密转UTF-8失败");
            inputBuffer = input.getBytes();
        }
        if (debug) {
            printHexString(inputBuffer);
        }

        return encrypt(inputBuffer, publicKey);
    }

    /**
     * 公钥加密
     *
     * @param inputBuffer 加密原文
     * @param publicKey   公钥
     * @return
     */
    public static byte[] encrypt(byte[] inputBuffer, ECPoint publicKey) {

        byte[] C1Buffer;
        ECPoint kpb;
        byte[] t;
        do {
            /* 1 产生随机数k，k属于[1, n-1] */
            BigInteger k = random(n);
            if (debug) {
                System.out.print("k: ");
                printHexString(k.toByteArray());
            }

            /* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
            ECPoint C1 = G.multiply(k);
            C1Buffer = C1.getEncoded(false);
            if (debug) {
                System.out.print("C1: ");
                printHexString(C1Buffer);
            }

            /*
             * 3 计算椭圆曲线点 S = [h]Pb
             */
            BigInteger h = ecc_bc_spec.getH();
            if (h != null) {
                ECPoint S = publicKey.multiply(h);
                if (S.isInfinity()) {
                    throw new IllegalStateException();
                }
            }
            kpb = publicKey.multiply(k).normalize();
            byte[] kpbBytes = kpb.getEncoded(false);
            t = KDF(kpbBytes, inputBuffer.length);
        } while (allZero(t));

        /* 6 计算C2=M^t */
        byte[] C2 = new byte[inputBuffer.length];
        for (int i = 0; i < inputBuffer.length; i++) {
            C2[i] = (byte) (inputBuffer[i] ^ t[i]);
        }

        /* 7 计算C3 = Hash(x2 || M || y2) */
        byte[] C3 = sm3hash(kpb.getXCoord().toBigInteger().toByteArray(), inputBuffer,
                kpb.getYCoord().toBigInteger().toByteArray());

        /* 8 输出密文 C=C1 || C2 || C3 */

        byte[] encryptResult = new byte[C1Buffer.length + C2.length + C3.length];

        System.arraycopy(C1Buffer, 0, encryptResult, 0, C1Buffer.length);
        System.arraycopy(C2, 0, encryptResult, C1Buffer.length, C2.length);
        System.arraycopy(C3, 0, encryptResult, C1Buffer.length + C2.length, C3.length);

        if (debug) {
            System.out.print("密文: ");
            printHexString(encryptResult);
        }

        return encryptResult;
    }


    /**
     * 私钥解密
     *
     * @param encryptData 密文数据字节数组
     * @param privateKey  解密私钥
     * @return
     */
    public static String decrypt(byte[] encryptData, BigInteger privateKey) {


        byte[] C1Byte = new byte[65];
        System.arraycopy(encryptData, 0, C1Byte, 0, C1Byte.length);

        ECPoint C1 = curve.decodePoint(C1Byte).normalize();

        /*
         * 计算椭圆曲线点 S = [h]C1 是否为无穷点
         */
        BigInteger h = ecc_bc_spec.getH();
        if (h != null) {
            ECPoint S = C1.multiply(h);
            if (S.isInfinity()) {
                throw new IllegalStateException();
            }
        }
        /* 计算[dB]C1 = (x2, y2) */
        ECPoint dBC1 = C1.multiply(privateKey).normalize();

        /* 计算t = KDF(x2 || y2, klen) */
        byte[] dBC1Bytes = dBC1.getEncoded(false);
        int klen = encryptData.length - 65 - DIGEST_LENGTH;
        byte[] t = KDF(dBC1Bytes, klen);

        if (allZero(t)) {
            System.err.println("all zero");
            throw new IllegalStateException();
        }

        /* 5 计算M'=C2^t */
        byte[] M = new byte[klen];
        for (int i = 0; i < M.length; i++) {
            M[i] = (byte) (encryptData[C1Byte.length + i] ^ t[i]);
        }
        if (debug) {
            printHexString(M);
        }

        /* 6 计算 u = Hash(x2 || M' || y2) 判断 u == C3是否成立 */
        byte[] C3 = new byte[DIGEST_LENGTH];


        System.arraycopy(encryptData, encryptData.length - DIGEST_LENGTH, C3, 0, DIGEST_LENGTH);
        byte[] u = sm3hash(dBC1.getXCoord().toBigInteger().toByteArray(), M,
                dBC1.getYCoord().toBigInteger().toByteArray());
        if (Arrays.equals(u, C3)) {

            try {
                return new String(M, "UTF8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return null;
        } else {
            if (debug) {
                System.out.print("u = ");
                printHexString(u);
                System.out.print("C3 = ");
                printHexString(C3);
                System.err.println("解密验证失败");
            }
            return null;
        }

    }


    public SM2Utils() {
        curve = new ECCurve.Fp(p, // q
                a, // a
                b); // b
        G = curve.createPoint(gx, gy);
        ecc_bc_spec = new ECDomainParameters(curve, G, n);
    }

    public SM2Utils(boolean debug) {
        this();
        SM2Utils.debug = debug;
    }


    /**
     * 字节数组拼接
     *
     * @param params
     * @return
     */
    private static byte[] join(byte[]... params) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] res = null;
        try {
            for (int i = 0; i < params.length; i++) {
                baos.write(params[i]);
            }
            res = baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return res;
    }

    /**
     * sm3摘要
     *
     * @param params
     * @return
     */
    private static byte[] sm3hash(byte[]... params) {
        byte[] res = null;
        try {
            res = SM3Utils.hash(join(params));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return res;
    }

    /**
     * 取得用户标识字节数组
     *
     * @param IDA
     * @param aPublicKey
     * @return
     */
    private static byte[] ZA(String IDA, ECPoint aPublicKey) {
        byte[] idaBytes = IDA.getBytes();
        int entlenA = idaBytes.length * 8;
        byte[] ENTLA = new byte[]{(byte) (entlenA & 0xFF00), (byte) (entlenA & 0x00FF)};
        byte[] ZA = sm3hash(ENTLA, idaBytes, a.toByteArray(), b.toByteArray(), gx.toByteArray(), gy.toByteArray(),
                aPublicKey.getXCoord().toBigInteger().toByteArray(),
                aPublicKey.getYCoord().toBigInteger().toByteArray());
        return ZA;
    }


    /**
     * 密钥派生函数
     *
     * @param Z
     * @param klen 生成klen字节数长度的密钥
     * @return
     */
    private static byte[] KDF(byte[] Z, int klen) {
        int ct = 1;
        int end = (int) Math.ceil(klen * 1.0 / 32);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            for (int i = 1; i < end; i++) {
                baos.write(sm3hash(Z, SM3Utils.toByteArray(ct)));
                ct++;
            }
            byte[] last = sm3hash(Z, SM3Utils.toByteArray(ct));
            if (klen % 32 == 0) {
                baos.write(last);
            } else {
                baos.write(last, 0, klen % 32);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 将十六进制字符串转成椭圆曲线点
     *
     * @param publicKey
     * @return ECPoint
     */
    public static ECPoint changeHexStrToECPoint(String publicKey) {
        //获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //提取公钥点
        ECPoint ecPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
        return ecPoint;
    }


    /**
     * 将十六进制字符串转成大整数
     *
     * @param privateKey
     * @return
     */
    public static BigInteger changeHexStrToBigInt(String privateKey) {
        return new BigInteger(privateKey, 16);
    }


    /**
     * 加密
     *
     * @param input
     * @param publicKey
     * @return
     */
    public static byte[] encrypt(String input, String publicKey) {
        ECPoint puk = changeHexStrToECPoint(publicKey);
        return encrypt(input, puk);
    }

    /**
     * 加密
     *
     * @param inputBuffer
     * @param publicKey
     * @return
     */
    public static byte[] encrypt(byte[] inputBuffer, String publicKey) {
        ECPoint puk = changeHexStrToECPoint(publicKey);
        return encrypt(inputBuffer, puk);
    }


    /**
     * 解密
     *
     * @param encryptData
     * @param privateKey
     * @return
     */
    public static String decrypt(byte[] encryptData, String privateKey) {
        if (64 == privateKey.length()) {
            privateKey = "00" + privateKey;
        }
        BigInteger prk = changeHexStrToBigInt(privateKey);
        return decrypt(encryptData, prk);
    }


    /**
     * 签名
     *
     * @param msgDigest
     * @param ID
     * @param privateKey
     * @return
     */
    public static String sign(String msgDigest, String ID, String privateKey) throws Exception {
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
                sm2ECParameters.getG(), sm2ECParameters.getN());
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), domainParameters);
        SM2Signer sm2Signer = new SM2Signer();
        sm2Signer.init(true, new ParametersWithID(new ParametersWithRandom(privateKeyParameters, SecureRandom.getInstance("SHA1PRNG")), ID.getBytes()));
        byte[] msg = msgDigest.getBytes(StandardCharsets.UTF_8);
        sm2Signer.update(msg, 0, msg.length);
        byte[] signature = sm2Signer.generateSignature();
        return Hex.toHexString(signature);
    }

    /**
     * 验签
     *
     * @param msgDigest
     * @param signature
     * @param accessKeyId
     * @param publicKey
     * @return
     */
    public static boolean verify(String msgDigest, String signature, String accessKeyId, String publicKey) throws Exception {
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
                sm2ECParameters.getG(), sm2ECParameters.getN());
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
        ParametersWithID parametersWithID = new ParametersWithID(publicKeyParameters, accessKeyId.getBytes());
        SM2Signer sm2Signer = new SM2Signer();
        sm2Signer.init(false, parametersWithID);
        byte[] msg = msgDigest.getBytes(StandardCharsets.UTF_8);
        sm2Signer.update(msg, 0, msg.length);
        return sm2Signer.verifySignature(Hex.decode(signature));
    }


    /**
     * SM2公钥加密
     *
     * @param plaintext 明文
     * @param publicKey 公钥
     * @return ciphertext 十六进制密文
     */
    public String encryptBySM2(String plaintext, String publicKey) {
        try {
            byte[] ciphertext = encrypt(plaintext, publicKey);
            return Hex.toHexString(ciphertext);
        } catch (Exception e) {
            throw new RuntimeException(SysEnum.ENCRYPT_FAILED.toString(), e);
        }

    }

    /**
     * SM2公钥加密
     *
     * @param plaintext
     * @return
     */
    public byte[] encryptBySM2(byte[] plaintext, String publicKey) {
        return encrypt(plaintext, publicKey);
    }

    /**
     * SM2私钥解密
     *
     * @param ciphertext 十六进制密文
     * @param privateKey 私钥
     * @return plaintext 明文
     */
    public String decryptBySM2(String ciphertext, String privateKey) {
        try {
            return decrypt(Hex.decode(ciphertext), privateKey);
        } catch (Exception e) {
            throw new RuntimeException(SysEnum.DECRYPT_FAILED.toString(), e);
        }
    }


    /**
     * SM2签名 with SM3
     *
     * @param message
     * @param ID
     * @param privateKey
     * @return
     */
    public static String signSm2WithSm3(String message, String ID, String privateKey) {
        try {
            String digest = SM3Utils.hashHex(message);
            log.info("sm3digesr===>:" + digest);
            return sign(digest, ID, privateKey);
        } catch (Exception e) {
            throw new RuntimeException(SysEnum.SIGN_FAILED.toString(), e);
        }

    }

    /**
     * SM3withSM2 验签
     *
     * @param message
     * @param signature
     * @param accessKeyId
     * @param publicKey
     * @return
     */
    public static boolean sm2WithSm3Verify(String message, String signature, String accessKeyId, String publicKey) {
        try {
            String digest = SM3Utils.hashHex(message);
            log.info("digest============>>:" + digest);
            return verify(digest, signature, accessKeyId, publicKey);
        } catch (Exception e) {
            throw new RuntimeException(SysEnum.VERIFY_SIGNATURE_FAILED.toString(), e);
        }
    }

    /**
     * 生成SM2公私钥对
     *
     * @return SM2KeyPair
     */
    public static SM2KeyPair generateSM2KeyPair() {
        BigInteger d = random(n.subtract(new BigInteger("1")));
        ECPoint ecPoint = G.multiply(d).normalize();
        String publicKey = Hex.toHexString(ecPoint.getEncoded(false));
        String privateKey = d.toString(16);
        SM2KeyPair sm2KeyPair = new SM2KeyPair(publicKey, privateKey);
        if (checkPublicKey(ecPoint)) {
            return sm2KeyPair;
        } else {
            return null;
        }
    }

    /**
     * 判断生成的公钥是否合法
     *
     * @param publicKey
     * @return
     */
    private static boolean checkPublicKey(ECPoint publicKey) {

        if (!publicKey.isInfinity()) {

            BigInteger x = publicKey.getXCoord().toBigInteger();
            BigInteger y = publicKey.getYCoord().toBigInteger();

            if (between(x, new BigInteger("0"), p) && between(y, new BigInteger("0"), p)) {
                BigInteger xResult = x.pow(3).add(a.multiply(x)).add(b).mod(p);
                BigInteger yResult = y.pow(2).mod(p);
                if (yResult.equals(xResult) && publicKey.multiply(n).isInfinity()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断是否在范围内
     *
     * @param param
     * @param min
     * @param max
     * @return
     */
    private static boolean between(BigInteger param, BigInteger min, BigInteger max) {
        if (param.compareTo(min) >= 0 && param.compareTo(max) < 0) {
            return true;
        } else {
            return false;
        }
    }
}
